/**
 * Copyright 2006 gworks.com.au
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. (http://www.apache.org/licenses/LICENSE-2.0)
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed 
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for 
 * the specific language governing permissions and limitations under the License.
 *
 * <dhamma-message src="Atisha (11th century Tibetan Buddhist master)">
 * 	The greatest achievement is selflessness. The greatest worth is self-mastery.
 * 	The greatest quality is seeking to serve others. The greatest precept is continual awareness.
 * 	The greatest medicine is the emptiness of everything. The greatest action is not conforming with the worlds ways.
 * 	The greatest magic is transmuting the passions. The greatest generosity is non-attachment.
 * 	The greatest goodness is a peaceful mind. The greatest patience is humility.
 * 	The greatest effort is not concerned with results. The greatest meditation is a mind that lets go.
 * 	The greatest wisdom is seeing through appearances. 
 * </dhamma-message>
 */
package org.javaongems.server;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.javaongems.client.service.GemletConstants;
import org.javaongems.runtime.http.ExpressRedirector;
import org.javaongems.runtime.io.PathUtils;

import com.google.gwt.user.client.rpc.SerializableException;


/**
 * <p>Providers a service-side representation of the GWT entry point page</p>
 * <p>Responsibilities:
 * <ul>
 * 	<li> inject bootstrap Gem properties javascript into the dispatch jsp
 * 	<li> create new Gem id 
 * 	<li> prepare on load failure details
 * </ul>
 * </p>
 * <p>Collaborators:
 * <ul>
 * 	<li> layout.jsp file
 * </ul>
 * </p>
 * @author Ashin Wimalajeewa (ash)
 */
abstract public class EntryPointPage implements GemletConstants {
    private static final Logger LOGGER = Logger.getLogger(EntryPointPage.class);

	final static public String HTML_JS_OPEN = "<script language=\"javascript\">";
	final static public String HTML_JS_GEM_DECL = "var " + GEM_JS_PROPERTY_OBJ + " = {};";
	final static public String HTML_JS_CLOSE = "</script>";
	final static public String HTML_JS_GEM_PROP_FUNC = 
									"\nfunction getGemProperty(elem) {\n" + 
									"  var ret = " + GEM_JS_PROPERTY_OBJ + "[elem];\n" +
									"  if (ret) return unescape(ret); else return null;\n" +
									"}\n";
		
	
	// defaults
	final static public String DEFAULT_GEM_GWT_RPC_CTX = "gwt-rpc/GemServe";
	
	// request attribute objects
	final static public String GEM_PAGE_OBJ = "GEM-PAGE-OBJ";
	final static public String GWT_MODULE_PARAM = "gwt:module";
	final static public String GWT_ENTRY_POINT_PAGE_PARAM = "gwt:entry-point-page";
	
	// cargo objects
	final static public String GEM_ON_LOAD_EXPCETION = "GEM-ON-LOAD_EXCEPTION";
	
	protected ServletContext servletContext;
	protected HttpServletRequest request;
	protected HttpServletResponse response;
	
	protected Map<String, Object>	cargo = new HashMap<String, Object>();
	protected Map<String, Object>	gemProps = new HashMap<String, Object>();
	
	/**
	 * Called from gem entry point pager immediately after the page object is
	 * created. This was done to avoid repeated constructor chains.
	 * @param req - the http servlet request associated with this page
	 * @param resp - the http servler response that this page may either write to
	 * 	or forward to
	 * @param gwtModule - the gwt module name associated with the entry point
	 * @param entryPointJsp - the jsp page that will get forwarded to after the
	 * 	page is built
	 */
	public void initialise(HttpServletRequest req, HttpServletResponse resp, String gwtModule, String entryPointJsp) {
		request = req;
		response = resp;
		cargo.put(GWT_MODULE_PARAM, gwtModule);
		cargo.put(GWT_ENTRY_POINT_PAGE_PARAM, entryPointJsp);
		servletContext = req.getSession().getServletContext();
		req.setAttribute(GEM_PAGE_OBJ, this);
		loadBaseProps();
	}
	
	/**
	 * loads the base properties that are independent of application
	 */
	private void loadBaseProps() {
		gemProps.put(GEM_ID, createGemId());
		String appCtx = PathUtils.appendSlashIfRequired(request.getContextPath());
		gemProps.put(GEM_APP_CTX, appCtx);
		gemProps.put(GEM_GWT_RPC_CTX, DEFAULT_GEM_GWT_RPC_CTX);
	}
	
	/**
	 * @return the gem id sourced from the gem properties
	 */
	public String getGemId() {
		return (String) gemProps.get(GEM_ID);
	}
	
	/**
	 * @return the application context sourced from the gem properties
	 */
	public String getApplicationContext() {
		return (String) gemProps.get(GEM_APP_CTX);
	}
	
	/**
	 * @return the gwt module name sourced from the gem properties
	 */
	public String getGwtModule() {
		return (String) gemProps.get(GWT_MODULE_PARAM);
	}
	
	/**
	 * @return the map of all gem properties that will get bootstrapped
	 * with the entry pages's html. These properties re-immerge on the
	 * client via the Gemlet's getGemProperty() method
	 */
	public Map<String, Object> getGemProperties() {
		return gemProps;
	}
	
	/**
	 * @return the volatile cargo that will exist for the duration of
	 * the entry page. Consider this as an alterative to 
	 * HttpServletRequest's getAttribute() feature
	 */
	public Map<String, Object> getCargo() {
		return cargo;
	}
	
	/**
	 * @return the http servlet request associated with this entry page
	 */
	public HttpServletRequest getRequest() {
		return request;
	}
	
	/**
	 * @return the http servlet response associated with this entry page
	 */
	public HttpServletResponse getResponse() {
		return response;
	}
	
	/**
	 * @return the servlet context from where servlet initialisation 
	 * parameters can be queried.
	 */
	public ServletContext getServletContext() {
		return servletContext;
	}
	
	/**
	 * Encodes the serializable exception into the gem properties such
	 * that the exception can be resuscitated back on the client when
	 * the Gemlet loads and invokes the handlePageLoadFailureIfAny() 
	 * method
	 * @param err - the serializable exception that is within the scope
	 * of the Gemlet
	 */
	protected void encodeOnLoadFailure(SerializableException err) {
		String className = err.getClass().getName();
		String message = err.getMessage();
		cargo.put(GEM_ON_LOAD_EXPCETION, err);
		gemProps.put(GEM_ON_LOAD_EXCEPTION_CLASS, className);
		gemProps.put(GEM_ON_LOAD_EXCEPTION_CLASS_MSG, message);
	}
	
	/**
	 * Forwards the http servlet response to a html entry point view
	 * representation. The view representation is sourced from the
	 * cargo using the "gwt:entry-point-page" parameter. If this
	 * returned value is null then no forward occurs.
	 * @throws IOException - see RequestDispatcher#forward
	 * @throws ServletException - see RequestDispatcher#forward
	 */
	protected void dispatch() throws IOException, ServletException {
		String entryPointDispatchJsp= (String) cargo.get(GWT_ENTRY_POINT_PAGE_PARAM);
		if (entryPointDispatchJsp == null)
			return;
		RequestDispatcher rd = servletContext.getRequestDispatcher(entryPointDispatchJsp);
		rd.forward(request, response);
	}

	/**
	 * @return the entire gem properties section (incl. javascript function) that 
	 * is to be injected into the jsp page. This will typically be called from
	 * a jsp taglet or jsp page.
	 */
	public String getGemPropertiesJsSection() {
		StringBuffer buff = new StringBuffer();
		writePropertiesSection(gemProps, buff);
		String section = buff.toString();
		return section;
	}
	
	/**
	 * Command equivalent to the getGemPropertiesJsSection() such that if a 
	 * Jsp writer was provided, it would write it straight into the writer.
	 * @param jspOutput - the jsp writer stream
	 * @throws IOException - if any write IO exceptions occur
	 */
	public void writeGemPropertiesJsSectionIntoJsp(Writer jspOutput) throws IOException {
		String section = getGemPropertiesJsSection();
		jspOutput.write(section);
	}
	
	/**
	 * Responsibilities:
	 * <ul>
	 * 	<li> first check whether a redirect is required, if so abort further operation
	 * 	<li> prepare the gem page properties
	 * 	<li> prepare the gem page content
	 * 	<li> dispatch the page to the JSP page
	 * </ul>
	 * @throws IOException
	 * @throws ServletException
	 * @see org.javaongems.server.GemServe.Action#execute(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
	 */
	public void render() throws IOException, ServletException {
		onRenderRedirectIfRequired();
		if (LOGGER.isDebugEnabled()) LOGGER.debug("bootstrapping gem page properties");
		registerGemProperties();
		try {
			if (LOGGER.isDebugEnabled()) LOGGER.debug("constructing gem page layout");
			buildPageContent();
		} catch (ExpressRedirector.ShortCircuit err) {
			throw err;
		} catch (Exception err) {
			SerializableException serr = recastAsSerializableException(err);
			encodeOnLoadFailure(serr);
			if (LOGGER.isDebugEnabled()) LOGGER.debug("preparing to tunnel exception", err);
		}
		dispatch();
	}
	
	/**
	 * Inspects the request's state and redirects to another context if 
	 * required. This event will be invoked immediately after the render 
	 * method is invoked, otherwise the page will follow a dispatch based
	 * scenario.
	 * @throws IOException - if a redirection failure on the http servlet response
	 * @throws ExpressRedirector - to bypass control logic
	 */
	abstract protected void onRenderRedirectIfRequired() throws IOException;

	/**
	 * Loads the bootstrapped gem properties into the page to enable the Gemlet 
	 * to function until another entry point page is requested.
	 */
	abstract protected void registerGemProperties();
	
	/**
	 * Loads the content elements into the page using the cargo facility. 
	 * This enables the page to be indexable by search engines and have enough 
	 * state locally to manage browser's back/forward navigations.
	 * @throws SerializableException if any client aware exception occurs. This
	 * exception will get serialised into the bootstrap properties and inspected 
	 * via the Gemlet::handlePageLoadFailureIfAny() method.
	 */
	abstract protected void buildPageContent() throws Exception;
	
	/**
	 * Recasts a given exception as a serialisable one that the client is
	 * aware of and able to handle in the handlePageLoadFailureIfAny
	 * method.  
	 * @param err - server side exception that was thrown
	 * @return the recasted exception such that it can be inspected
	 * from the javascript client. 
	 */
	abstract protected SerializableException recastAsSerializableException(Exception err);
	
	
	/**
	 * Construct a gem id based on:
	 * <ul>
	 * 	<li> the current server-side thread's hashcode
	 * 	<li> hash code of the current time
	 * </ul>
	 * @return a relatively unique gem id  
	 */
	private String createGemId() {
		int threadHc = Thread.currentThread().hashCode();
		int timeHc = (new Date()).hashCode();
		StringBuffer ret = new StringBuffer();
		ret.append(threadHc).append('-').append(timeHc);
		return ret.toString();
	}
	
	static private void writePropertiesSection(Map gemProps, StringBuffer out) {
		out.append(HTML_JS_OPEN).append("\n");
		out.append(HTML_JS_GEM_DECL).append("\n");
		for (Iterator iter = gemProps.keySet().iterator(); iter.hasNext();) {
			String name = (String) iter.next();
			Object val = gemProps.get(name);
			if (val == null)
				continue;
			String valAsStr = val.toString();
			writePropertyEntry(name, valAsStr, out);
		}
		out.append(HTML_JS_GEM_PROP_FUNC);
		out.append(HTML_JS_CLOSE).append("\n");
	}
	
	static private void writePropertyEntry(String name, String val, StringBuffer out) {
		String encVal;
		try {
			 encVal = URLEncoder.encode(val, "UTF-8");
		} catch (UnsupportedEncodingException err) {
			encVal = "failed";
			LOGGER.error("unable to encode property value:[" + val + "]", err);
		}
		out.append(GEM_JS_PROPERTY_OBJ).append("['").append(name).append("']=\"").append(encVal).append("\";\n");
	}
}